{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- https://liaoxuefeng.com/books/java/springcloud/project/index.html"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "对于Warp Exchange项目，我们以Maven为构建工具，把每个模块作为一个Maven的项目管理，并抽取出公共逻辑放入`common`模块，结构如下：\n",
    "\n",
    "- common：公共代码；\n",
    "- config：配置服务器；\n",
    "- push：推送服务；\n",
    "- quotation：行情服务；\n",
    "- trading-api：交易API服务；\n",
    "- trading-engine：交易引擎；\n",
    "- trading-sequencer：定序服务；\n",
    "- ui：用户Web界面。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "为了简化版本和依赖管理，我们用`parent`模块管理最基础的`pom.xml`，其他模块直接从`parent`继承，能大大简化各自的`pom.xml`。\n",
    "\n",
    "`parent`模块`pom.xml`内容如下："
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- [parent的pom.xml](./step-1/parent/pom.xml)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "上述`pom.xml`中，除了写死的Spring Boot版本、Java运行版本、项目版本外，其他引入的版本均以`<xxx.version>1.23</xxx.version>`的形式定义，以便后续可以用`${xxx.version}`引用版本号，避免了同一个组件出现多个写死的版本定义。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "对其他业务模块，引入`parent`的`pom.xml`可大大简化配置。\n",
    "\n",
    "以`ui`模块为例，其`pom.xml`如下："
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- [ui的pom.xml](./step-1/ui/pom.xml)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "因为我们在`parent`的`pom.xml`中引入了Spring Cloud的依赖管理，因此，无需指定相关组件的版本。只有我们自己编写的组件和未在Spring Boot和Spring Cloud中引入的组件，才需要指定版本。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "最后，我们还需要一个`build`模块，把所有模块放到一起编译。建立`build`文件夹并创建`pom.xml`如下："
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- [build的pom.xml](./step-1/build/pom.xml)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们还需要创建目录`config-repo`来存储Spring Cloud Config服务器端的配置文件。\n",
    "\n",
    "最后，将所有模块导入IDE，可正常开发、编译、运行。如果要在命令行模式下运行，进入`build`文件夹使用Maven编译即可："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [],
   "source": [
    "! cd ./step-1/build && mvn clean package"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 本地开发环境"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在本地开发时，我们需要经常调试代码。除了安装JDK，选择一个IDE外，我们还需要在本地运行MySQL、Redis、Kafka，以及Kafka依赖的ZooKeeper服务。\n",
    "\n",
    "考虑到手动安装各个服务在不同操作系统下的差异，以及初始化数据非常麻烦，我们使用[Docker Desktop](https://www.docker.com/products/docker-desktop/)来运行这些基础服务，需要在`build`目录下编写一个`docker-compose.yml`文件定义我们要运行的所有服务："
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- [docker-compose.yml](./step-1/build/docker-compose.yml)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在上述`docker-compose.yml`文件中，我们定义了MySQL、Redis、Kafka以及Kafka依赖的ZooKeeper服务，各服务均暴露标准端口，且MySQL的`root`口令设置为`password`，第一次启动MySQL时，使用`sql/schema.sql`文件初始化数据库表结构。所有数据盘均挂载到`build`目录下的`docker`目录。\n",
    "\n",
    "在`build`目录下运行`docker-compose up -d`即可启动容器："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      " Container zookeeper  Running\n",
      " Container redis  Running\n",
      " Container kafka  Running\n",
      " Container mysql  Starting\n",
      " Container mysql  Started\n"
     ]
    }
   ],
   "source": [
    "! cd ./step-1/build && sudo docker-compose up -d"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> - 国内源被禁了, 有个收费的https://xuanyuan.cloud/\n",
    "> - bitnami改为bitnamilegacy"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在Docker Desktop中可看到运行状态："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "REPOSITORY                TAG       IMAGE ID       CREATED       SIZE\n",
      "redis                     6.2       d06ff6ec778c   5 days ago    157MB\n",
      "mysql                     8.0       4a8843ef1c30   2 weeks ago   1.07GB\n",
      "bitnamilegacy/kafka       3.0       c0a0a0bae960   2 years ago   881MB\n",
      "bitnamilegacy/zookeeper   3.5       fb73abba8513   3 years ago   742MB\n"
     ]
    }
   ],
   "source": [
    "! docker images"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "CONTAINER ID   IMAGE                         COMMAND                  CREATED         STATUS         PORTS                                         NAMES\n",
      "eaaec0a53fbb   bitnamilegacy/kafka:3.0       \"/opt/bitnami/script…\"   4 minutes ago   Up 4 minutes   0.0.0.0:9092->9092/tcp, [::]:9092->9092/tcp   kafka\n",
      "374f3ebb0e53   redis:6.2                     \"docker-entrypoint.s…\"   4 minutes ago   Up 4 minutes   0.0.0.0:6379->6379/tcp, [::]:6379->6379/tcp   redis\n",
      "43d39f3bca1a   bitnamilegacy/zookeeper:3.5   \"/opt/bitnami/script…\"   4 minutes ago   Up 4 minutes   0.0.0.0:2181->2181/tcp, [::]:2181->2181/tcp   zookeeper\n"
     ]
    }
   ],
   "source": [
    "! docker ps"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "如果要删除开发环境的所有数据，首先停止运行Docker容器进程并删除，然后删除`build`目录下的`docker`目录，重新运行`docker-compose`即可。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "vscode": {
     "languageId": "raw"
    }
   },
   "source": [
    "**容器启动失败**\n",
    "- **WSL2 环境**：确保 `./docker/zookeeper-data` 位于 WSL 的 Linux 文件系统（例如 `/home/<user>/project/docker/zookeeper-data`），避免放在 `/mnt/c/...`。\n",
    "- 修改目录权限\n",
    "\n",
    "![container_status](./images/container_status.png)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [],
   "source": [
    "! cd ./step-1/build && sudo chown -R 1001:1001 * "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [],
   "source": [
    "! cd ./step-1/build && sudo chmod -R 775 *"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [],
   "source": [
    "! sudo docker-compose up -d"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "CONTAINER ID   IMAGE                         COMMAND                  CREATED              STATUS              PORTS                                         NAMES\n",
      "489f1f69bb64   bitnamilegacy/kafka:3.0       \"/opt/bitnami/script…\"   About a minute ago   Up About a minute   0.0.0.0:9092->9092/tcp, [::]:9092->9092/tcp   kafka\n",
      "0ba9695b0c92   bitnamilegacy/zookeeper:3.5   \"/opt/bitnami/script…\"   About a minute ago   Up About a minute   0.0.0.0:2181->2181/tcp, [::]:2181->2181/tcp   zookeeper\n",
      "d171693f6482   redis:6.2                     \"docker-entrypoint.s…\"   4 minutes ago        Up 3 minutes        0.0.0.0:6379->6379/tcp, [::]:6379->6379/tcp   redis\n",
      "a2d4559bb5fb   mysql:8.0                     \"docker-entrypoint.s…\"   4 minutes ago        Up 3 minutes        0.0.0.0:3306->3306/tcp, [::]:3306->3306/tcp   mysql\n"
     ]
    }
   ],
   "source": [
    "! docker ps"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Spring Cloud Config"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Spring Cloud Config是Spring Cloud的一个子项目，它的主要目的是解决多个Spring Boot应用启动时，应该如何读取配置文件的问题。\n",
    "\n",
    "对于单体应用，即一个独立的Spring Boot应用，我们会把配置写在`application.yml`文件中。如果配置需要针对多个环境，可以用`---`分隔并标注好环境："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "yaml"
    }
   },
   "outputs": [],
   "source": [
    "# application.yml\n",
    "# 通用配置:\n",
    "spring:\n",
    "  datasource:\n",
    "    url: jdbc:mysql://localhost/test\n",
    "\n",
    "---\n",
    "\n",
    "# test profile:\n",
    "spring:\n",
    "  config:\n",
    "    activate:\n",
    "      on-profile: test\n",
    "  datasource:\n",
    "    url: jdbc:mysql://172.16.0.100/test"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这种配置方式针对单个Spring Boot应用是可行的，但是，针对分布式应用，有多个Spring Boot应用需要启动时，分散在各个应用中的配置既不便于管理，也不便于复用相同的配置。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Spring Cloud Config提供了一个通用的分布式应用的配置解决方案。它把配置分为两部分：\n",
    "\n",
    "- Config Server：配置服务器，负责读取所有配置；\n",
    "- Config Client：嵌入到各个Spring Boot应用中，本地无配置信息，启动时向服务器请求配置。\n",
    "\n",
    "我们先来看看如何搭建一个Spring Cloud Config Server，即配置服务器。\n",
    "\n",
    "首先，在`config`模块中引入`spring-cloud-config-server`依赖："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "xml"
    }
   },
   "outputs": [],
   "source": [
    "<dependency>\n",
    "    <groupId>org.springframework.cloud</groupId>\n",
    "    <artifactId>spring-cloud-config-server</artifactId>\n",
    "</dependency>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "然后，编写一个`ConfigApplication`入口，标注`@EnableConfigServer`："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "java"
    }
   },
   "outputs": [],
   "source": [
    "@EnableConfigServer\n",
    "@SpringBootApplication\n",
    "public class ConfigApplication {\n",
    "    public static void main(String[] args) {\n",
    "        SpringApplication.run(ConfigApplication.class, args);\n",
    "    }\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "最后，在`application.yml`中设置如何搜索配置。Spring Cloud Config支持多种配置方式，包括从本地文件、Git仓库、数据库等多个地方读取配置。这里我们选择以本地文件的方式读取配置文件，这也是最简单的一种配置方式：\n",
    "\n",
    "- [application.yml](./step-1/config/src/main/resources/application.yml)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在`config-repo`目录下，存放的就是一系列配置文件："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      ".\n",
      "├── application-default.yml\n",
      "├── application-test.yml\n",
      "├── application.yml\n",
      "├── push.yml\n",
      "├── quotation.yml\n",
      "├── trading-api.yml\n",
      "├── trading-engine.yml\n",
      "├── trading-sequencer.yml\n",
      "├── ui-default.yml\n",
      "└── ui.yml\n",
      "\n",
      "0 directories, 10 files\n"
     ]
    }
   ],
   "source": [
    "! cd ./step-1/config-repo && tree"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "至此，配置服务器就完成了，直接运行`ConfigApplication`即可启动配置服务器。在开发过程中，保持配置服务器在后台运行即可。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "接下来，对于每个负责业务的Spring Boot应用，我们需要从Spring Cloud Config Server读取配置。读取配置并不是说本地零配置，还是需要一点基础配置信息。以`ui`项目为例，编写`application.yml`如下：\n",
    "\n",
    "[application.yml](./step-1/ui/src/main/resources/application.yml)\n",
    "\n",
    "上述默认的Config Server配置为`http://localhost:8888`，也可以通过环境变量指定Config Server的地址。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "下一步是在`ui`模块的[`pom.xml`](./step-1/ui/pom.xml)中添加依赖："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "xml"
    }
   },
   "outputs": [],
   "source": [
    "<dependency>\n",
    "    <groupId>org.springframework.cloud</groupId>\n",
    "    <artifactId>spring-cloud-starter-config</artifactId>\n",
    "</dependency>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "接下来正常启动`UIApplication`，该应用就会自动从Config Server读取配置。由于我们指定了应用的名称是`ui`，且默认的`profile`是`default`，因此，Config Server将返回以下4个配置文件：\n",
    "\n",
    "- ui-default.yml\n",
    "- application-default.yml\n",
    "- ui.yml\n",
    "- application.yml"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "前面的配置文件优先级较高，后面的配置文件优先级较低。如果出现相同的配置项，则在优先级高的配置生效。\n",
    "\n",
    "我们可以在浏览器访问[`http://localhost:8888/ui/default`](http://localhost:8888/ui/default)看到Config Server返回的配置，它是一个JSON文件："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [],
   "source": [
    "! cd step-1/config && mvn spring-boot:run # 启动 Spring Cloud Config Server, http://localhost:8888/ui/default"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "json"
    }
   },
   "outputs": [],
   "source": [
    "{\n",
    "    \"name\": \"ui\",\n",
    "    \"profiles\": [\n",
    "        \"default\"\n",
    "    ],\n",
    "    \"label\": null,\n",
    "    \"version\": null,\n",
    "    \"state\": null,\n",
    "    \"propertySources\": [\n",
    "        {\n",
    "            \"name\": \"file:../config-repo/ui-default.yml\",\n",
    "            \"source\": {\n",
    "                \"pebble.cache\": false\n",
    "            }\n",
    "        },\n",
    "        {\n",
    "            \"name\": \"file:../config-repo/application-default.yml\",\n",
    "            \"source\": {\n",
    "                \"exchange.config.debug-mode\": \"${EXCHANGE_DEBUG_MODE:true}\",\n",
    "                \"logging.level.com.itranswarp.exchange\": \"${LOGGING_EXCHANGE_LEVEL:DEBUG}\"\n",
    "            }\n",
    "        },\n",
    "        {\n",
    "            \"name\": \"file:../config-repo/ui.yml\",\n",
    "            \"source\": {\n",
    "                \"server.port\": \"${APP_PORT:8000}\",\n",
    "                \"pebble.prefix\": \"/templates/\",\n",
    "                \"pebble.suffix\": \".html\"\n",
    "            }\n",
    "        },\n",
    "        {\n",
    "            \"name\": \"file:../config-repo/application.yml\",\n",
    "            \"source\": {\n",
    "                \"server.tomcat.use-forward-headers\": \"${TOMCAT_USE_FORWARD_HEADERS:true}\",\n",
    "                \"server.tomcat.protocol-header\": \"${TOMCAT_PROTOCOL_HEADER:x-forwarded-proto}\",\n",
    "                \"exchange.config.order-book-depth\": \"${ORDER_BOOK_DEPTH:50}\",\n",
    "                \"exchange.config.debug-mode\": \"${EXCHANGE_DEBUG_MODE:false}\",\n",
    "                \"exchange.config.time-zone\": \"${EXCHANGE_TIME_ZONE:}\",\n",
    "                \"exchange.config.hmac-key\": \"${EXCHANGE_HMAC_KEY:ChangeThisHmacKey4ExchangeConfig}\",\n",
    "                \"exchange.config.session-timeout\": \"${EXCHANGE_SESSION_TIMEOUT:P3D}\",\n",
    "                \"exchange.config.api-endpoints.trading-api\": \"${TRADING_API:http://localhost:8001}\",\n",
    "                \"exchange.config.api-endpoints.trading-engine-api\": \"${TRADING_ENGINE_API:http://localhost:8002}\",\n",
    "                \"exchange.risk-control.notify-message-expires\": \"${NOTIFY_MESSAGE_EXPIRES:PT60S}\",\n",
    "                \"exchange.risk-control.transfer-out-message-expires\": \"${TRANSFER_OUT_MESSAGE_EXPIRES:P7D}\",\n",
    "                \"exchange.setting.scheduler.cron.reload-trading-metadata\": \"${CRON_RELOAD_TRADING_METADATA:0 */10 * * * *}\",\n",
    "                \"exchange.setting.scheduler.cron.update-latest-price\": \"${CRON_UPDATE_LATEST_PRICE:0,10,20,30,40,50 * * * * *}\",\n",
    "                \"exchange.setting.scheduler.fixed-delay.flush-orderbook\": \"${FIXED_DELAY_FLUSH_ORDERBOOK:500}\",\n",
    "                \"exchange.setting.scheduler.fixed-delay.reinvoke-transfer\": \"${FIXED_DELAY_REINVOKE_TRANSFER:60000}\",\n",
    "                \"exchange.setting.scheduler.fixed-delay.reload-trading-metadata\": \"${FIXED_DELAY_RELOAD_TRADING_METADATA:600000}\",\n",
    "                \"exchange.setting.scheduler.fixed-delay.save-sequence-id\": \"${FIXED_DELAY_SAVE_SEQUENCE_ID:60000}\",\n",
    "                \"exchange.setting.timezone\": \"${EXCHANGE_TIMEZONE:}\",\n",
    "                \"exchange.setting.aes-key\": \"${EXCHANGE_AES_KEY:CryptoExchangeAESRandomSecureKey}\",\n",
    "                \"exchange.setting.endpoint.ui\": \"${UI_ENDPOINT:http://localhost:8000}\",\n",
    "                \"exchange.setting.endpoint.meta-api\": \"${META_API_ENDPOINT:http://localhost:8001}\",\n",
    "                \"exchange.setting.endpoint.wallet-api\": \"${WALLET_API_ENDPOINT:http://localhost:8901}\",\n",
    "                \"exchange.setting.endpoint.spots-api\": \"${SPOTS_API_ENDPOINT:http://localhost:8101}\",\n",
    "                \"exchange.setting.endpoint.spots-trading-internal-api\": \"${SPOTS_TRADING_INTERNAL_API_ENDPOINT:http://localhost:8103}\",\n",
    "                \"spring.kafka.bootstrap-servers\": \"${KAFKA_SERVERS:localhost:9092}\",\n",
    "                \"spring.kafka.consumer.group-id\": \"${KAFKA_CONSUMER_GROUP_ID:}\",\n",
    "                \"spring.kafka.consumer.auto-offset-reset\": \"${KAFKA_CONSUMER_AUTO_OFFSET_RESET:latest}\",\n",
    "                \"spring.kafka.consumer.max-poll-records\": \"${KAFKA_CONSUMER_MAX_POLL_RECORDS:500}\",\n",
    "                \"spring.kafka.consumer.max-partition-fetch-bytes\": \"${KAFKA_MAX_PARTITION_FETCH_BYTES:1000000}\",\n",
    "                \"spring.redis.host\": \"${REDIS_HOST:localhost}\",\n",
    "                \"spring.redis.port\": \"${REDIS_PORT:6379}\",\n",
    "                \"spring.redis.password\": \"${REDIS_PASSWORD:}\",\n",
    "                \"spring.redis.database\": \"${REDIS_DATABASE:0}\",\n",
    "                \"spring.datasource.url\": \"jdbc:mysql://${DB_HOST:localhost}/${DB_NAME:exchange}?useSSL=false&allowMultiQueries=true&useUnicode=true&characterEncoding=utf8\",\n",
    "                \"spring.datasource.username\": \"${DB_USERNAME:root}\",\n",
    "                \"spring.datasource.password\": \"${DB_PASSWORD:password}\",\n",
    "                \"spring.datasource.driver-class-name\": \"com.mysql.cj.jdbc.Driver\",\n",
    "                \"spring.datasource.hikari.auto-commit\": false,\n",
    "                \"spring.datasource.hikari.connection-timeout\": \"${DB_TIMEOUT:3000}\",\n",
    "                \"spring.datasource.hikari.validation-timeout\": \"${DB_TIMEOUT:3000}\",\n",
    "                \"spring.datasource.hikari.max-lifetime\": 600000,\n",
    "                \"spring.datasource.hikari.maximum-pool-size\": \"${DB_POOL_MAX:500}\",\n",
    "                \"spring.datasource.hikari.minimum-idle\": \"${DB_POOL_IDEL:1}\",\n",
    "                \"logging.level.root\": \"${LOGGING_ROOT_LEVEL:INFO}\",\n",
    "                \"logging.level.org.apache.kafka\": \"${LOGGING_KAFKA_LEVEL:WARN}\",\n",
    "                \"logging.level.com.itranswarp.exchange\": \"${LOGGING_EXCHANGE_LEVEL:INFO}\"\n",
    "            }\n",
    "        }\n",
    "    ]\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "如果我们启动`UIApplication`时传入`SPRING_PROFILES_ACTIVE=test`，将profile设置为`test`，则Config Server返回的文件如下：\n",
    "\n",
    "- ui-test.yml\n",
    "- application-test.yml\n",
    "- ui.yml\n",
    "- application.yml\n",
    "\n",
    "可以通过[http://localhost:8888/ui/test](http://localhost:8888/ui/test)查看返回的配置。由于文件`ui-test.yml`不存在，因此，实际配置由3个文件合并而成。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "json"
    }
   },
   "outputs": [],
   "source": [
    "{\n",
    "    \"name\": \"ui\",\n",
    "    \"profiles\": [\n",
    "        \"test\"\n",
    "    ],\n",
    "    \"label\": null,\n",
    "    \"version\": null,\n",
    "    \"state\": null,\n",
    "    \"propertySources\": [\n",
    "        {\n",
    "            \"name\": \"file:../config-repo/application-test.yml\",\n",
    "            \"source\": {\n",
    "                \"exchange.config.debug-mode\": \"${EXCHANGE_DEBUG_MODE:true}\",\n",
    "                \"logging.level.root\": \"${LOGGING_ROOT_LEVEL:DEBUG}\"\n",
    "            }\n",
    "        },\n",
    "        {\n",
    "            \"name\": \"file:../config-repo/ui.yml\",\n",
    "            \"source\": {\n",
    "                \"server.port\": \"${APP_PORT:8000}\",\n",
    "                \"pebble.prefix\": \"/templates/\",\n",
    "                \"pebble.suffix\": \".html\"\n",
    "            }\n",
    "        },\n",
    "        {\n",
    "            \"name\": \"file:../config-repo/application.yml\",\n",
    "            \"source\": {\n",
    "                \"server.tomcat.use-forward-headers\": \"${TOMCAT_USE_FORWARD_HEADERS:true}\",\n",
    "                \"server.tomcat.protocol-header\": \"${TOMCAT_PROTOCOL_HEADER:x-forwarded-proto}\",\n",
    "                \"exchange.config.order-book-depth\": \"${ORDER_BOOK_DEPTH:50}\",\n",
    "                \"exchange.config.debug-mode\": \"${EXCHANGE_DEBUG_MODE:false}\",\n",
    "                \"exchange.config.time-zone\": \"${EXCHANGE_TIME_ZONE:}\",\n",
    "                \"exchange.config.hmac-key\": \"${EXCHANGE_HMAC_KEY:ChangeThisHmacKey4ExchangeConfig}\",\n",
    "                \"exchange.config.session-timeout\": \"${EXCHANGE_SESSION_TIMEOUT:P3D}\",\n",
    "                \"exchange.config.api-endpoints.trading-api\": \"${TRADING_API:http://localhost:8001}\",\n",
    "                \"exchange.config.api-endpoints.trading-engine-api\": \"${TRADING_ENGINE_API:http://localhost:8002}\",\n",
    "                \"exchange.risk-control.notify-message-expires\": \"${NOTIFY_MESSAGE_EXPIRES:PT60S}\",\n",
    "                \"exchange.risk-control.transfer-out-message-expires\": \"${TRANSFER_OUT_MESSAGE_EXPIRES:P7D}\",\n",
    "                \"exchange.setting.scheduler.cron.reload-trading-metadata\": \"${CRON_RELOAD_TRADING_METADATA:0 */10 * * * *}\",\n",
    "                \"exchange.setting.scheduler.cron.update-latest-price\": \"${CRON_UPDATE_LATEST_PRICE:0,10,20,30,40,50 * * * * *}\",\n",
    "                \"exchange.setting.scheduler.fixed-delay.flush-orderbook\": \"${FIXED_DELAY_FLUSH_ORDERBOOK:500}\",\n",
    "                \"exchange.setting.scheduler.fixed-delay.reinvoke-transfer\": \"${FIXED_DELAY_REINVOKE_TRANSFER:60000}\",\n",
    "                \"exchange.setting.scheduler.fixed-delay.reload-trading-metadata\": \"${FIXED_DELAY_RELOAD_TRADING_METADATA:600000}\",\n",
    "                \"exchange.setting.scheduler.fixed-delay.save-sequence-id\": \"${FIXED_DELAY_SAVE_SEQUENCE_ID:60000}\",\n",
    "                \"exchange.setting.timezone\": \"${EXCHANGE_TIMEZONE:}\",\n",
    "                \"exchange.setting.aes-key\": \"${EXCHANGE_AES_KEY:CryptoExchangeAESRandomSecureKey}\",\n",
    "                \"exchange.setting.endpoint.ui\": \"${UI_ENDPOINT:http://localhost:8000}\",\n",
    "                \"exchange.setting.endpoint.meta-api\": \"${META_API_ENDPOINT:http://localhost:8001}\",\n",
    "                \"exchange.setting.endpoint.wallet-api\": \"${WALLET_API_ENDPOINT:http://localhost:8901}\",\n",
    "                \"exchange.setting.endpoint.spots-api\": \"${SPOTS_API_ENDPOINT:http://localhost:8101}\",\n",
    "                \"exchange.setting.endpoint.spots-trading-internal-api\": \"${SPOTS_TRADING_INTERNAL_API_ENDPOINT:http://localhost:8103}\",\n",
    "                \"spring.kafka.bootstrap-servers\": \"${KAFKA_SERVERS:localhost:9092}\",\n",
    "                \"spring.kafka.consumer.group-id\": \"${KAFKA_CONSUMER_GROUP_ID:}\",\n",
    "                \"spring.kafka.consumer.auto-offset-reset\": \"${KAFKA_CONSUMER_AUTO_OFFSET_RESET:latest}\",\n",
    "                \"spring.kafka.consumer.max-poll-records\": \"${KAFKA_CONSUMER_MAX_POLL_RECORDS:500}\",\n",
    "                \"spring.kafka.consumer.max-partition-fetch-bytes\": \"${KAFKA_MAX_PARTITION_FETCH_BYTES:1000000}\",\n",
    "                \"spring.redis.host\": \"${REDIS_HOST:localhost}\",\n",
    "                \"spring.redis.port\": \"${REDIS_PORT:6379}\",\n",
    "                \"spring.redis.password\": \"${REDIS_PASSWORD:}\",\n",
    "                \"spring.redis.database\": \"${REDIS_DATABASE:0}\",\n",
    "                \"spring.datasource.url\": \"jdbc:mysql://${DB_HOST:localhost}/${DB_NAME:exchange}?useSSL=false&allowMultiQueries=true&useUnicode=true&characterEncoding=utf8\",\n",
    "                \"spring.datasource.username\": \"${DB_USERNAME:root}\",\n",
    "                \"spring.datasource.password\": \"${DB_PASSWORD:password}\",\n",
    "                \"spring.datasource.driver-class-name\": \"com.mysql.cj.jdbc.Driver\",\n",
    "                \"spring.datasource.hikari.auto-commit\": false,\n",
    "                \"spring.datasource.hikari.connection-timeout\": \"${DB_TIMEOUT:3000}\",\n",
    "                \"spring.datasource.hikari.validation-timeout\": \"${DB_TIMEOUT:3000}\",\n",
    "                \"spring.datasource.hikari.max-lifetime\": 600000,\n",
    "                \"spring.datasource.hikari.maximum-pool-size\": \"${DB_POOL_MAX:500}\",\n",
    "                \"spring.datasource.hikari.minimum-idle\": \"${DB_POOL_IDEL:1}\",\n",
    "                \"logging.level.root\": \"${LOGGING_ROOT_LEVEL:INFO}\",\n",
    "                \"logging.level.org.apache.kafka\": \"${LOGGING_KAFKA_LEVEL:WARN}\",\n",
    "                \"logging.level.com.itranswarp.exchange\": \"${LOGGING_EXCHANGE_LEVEL:INFO}\"\n",
    "            }\n",
    "        }\n",
    "    ]\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们可以很容易地看到，一个Spring Boot应用在启动时，首先要设置自己的`name`并导入Config Server的URL，再根据当前活动的`profile`，由Config Server返回多个配置文件：\n",
    "\n",
    "- {name}-{profile}.yml\n",
    "- application-{profile}.yml\n",
    "- {name}.yml\n",
    "- application.yml"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "其中，`{name}-{xxx}.yml`是针对某个应用+某个`profile`的特定配置，`{name}.yml`是针对某个应用+所有profile的配置，`application-{profile}.yml`是针对某个`profile`的全局配置，`application.yml`是所有应用的全局配置。搭配各种配置文件就可以灵活组合配置。一般来说，全局默认的配置放在`application.yml`中，例如数据库连接："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "yaml"
    }
   },
   "outputs": [],
   "source": [
    "spring:\n",
    "  datasource:\n",
    "    url: jdbc:mysql://localhost/test"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这样保证了默认连接到本地数据库，在生产环境中会直接报错而不是连接到错误的数据库。\n",
    "\n",
    "在生产环境，例如`profile`设置为`prod`，则可以将数据库连接写在`application-prod.yml`中，使得所有生产环境的应用读取到的数据库连接是一致的："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "yaml"
    }
   },
   "outputs": [],
   "source": [
    "spring:\n",
    "  datasource:\n",
    "    url: jdbc:mysql://172.16.0.100/prod_db"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "某个应用自己特定的配置则应当放到`{name}.yml`和`{name}-{profile}.yml`中。\n",
    "\n",
    "在设置好各个配置文件后，应当通过浏览器检查Config Server返回的配置是否符合预期。\n",
    "\n",
    "Spring Cloud Config还支持配置多个profile，以及从加密的配置源读取配置等。如果遇到更复杂的需求，可参考[Spring Cloud Config的文档](https://spring.io/projects/spring-cloud-config#learn)。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 环境变量"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "需要特别注意，在`config-repo`的配置文件里，使用的环境变量，不是Config Server的环境变量，而是具体某个Spring Boot应用的环境变量。\n",
    "\n",
    "我们举个例子：假定`ui.yml`定义如下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "yaml"
    }
   },
   "outputs": [],
   "source": [
    "server:\n",
    "  port: ${APP_PORT:8000}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "当`UIApplication`启动时，它获得的配置为`server.port=${APP_PORT:8000}`。Config Server*不会替换任何环境变量*，而是将它们原封不动地返回给`UIApplication`，由`UIApplication`根据自己的环境变量解析后获得最终配置。如果我们启动`UIApplication`时传入环境变量："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [],
   "source": [
    "! java -DAPP_PORT=7000 -jar ui.jar"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "则`UIApplication`最终读取的配置`server.port`为`7000`。\n",
    "\n",
    "可见，使用Spring Cloud Config时，读取配置文件步骤如下：\n",
    "\n",
    "1. 启动XxxApplication时，读取自身的`application.yml`，获得`name`和Config Server地址；\n",
    "2. 根据`name`、`profile`和Config Server地址，获得一个或多个有优先级的配置文件；\n",
    "3. 按优先级合并配置项；\n",
    "4. 如果配置项中存在环境变量，则使用Xxx应用本身的环境变量去替换占位符。\n",
    "\n",
    "环境变量通常用于配置一些敏感信息，如数据库连接口令，它们不适合明文写在`config-repo`的配置文件里。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 常见错误"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "启动一个Spring Boot应用时，如果出现`Unable to load config data`错误：\n",
    "\n",
    "```shell\n",
    "java.lang.IllegalStateException: Unable to load config data from 'configserver:http://localhost:8888'\n",
    "\tat org.springframework.boot.context.config.StandardConfigDataLocationResolver.getReferences\n",
    "    at ...\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "需要检查是否在`pom.xml`中引入了`spring-cloud-starter-config`，因为没有引入该依赖时，应用无法解析本地配置的`import: configserver:xxx`。\n",
    "\n",
    "如果在启动一个Spring Boot应用时，Config Server没有运行，通常错误信息是因为没有读取到配置导致无法创建某个Bean。"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "C++17",
   "language": "C++17",
   "name": "xcpp17"
  },
  "language_info": {
   "codemirror_mode": "text/x-c++src",
   "file_extension": ".cpp",
   "mimetype": "text/x-c++src",
   "name": "c++",
   "version": "17"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
